/*
The MIT License

Copyright (c) 2011 Sean M. Thomas

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

#define USE_PROFILER

#ifdef _WIN32
#include <windows.h>
#endif

#ifdef _DEBUG
#include "crtdbg.h"
#endif

#include <GL/glew.h>
#include <GL/glut.h>
#include <SOIL.h>

#include "deferredRenderer.h"
#include "forwardRenderer.h"

#include <glm.hpp>
#include <gtc/matrix_transform.hpp>
using namespace glm;

#include "fs_object.h"

#include <iostream>
#include <stdio.h>

#include "assimp.hpp"
#include "DefaultLogger.h"
#include "LogStream.h"

#include "Camera.h"

#include "fs_containers.h"

#include "shaderInit.h"
#include "forwardRenderer.h"

// For setting vsync
#include <GL/wglew.h>

#include <vector>
using namespace std;

#include "street.h"
#include "fs_TexturePool.h"
#include "lightPool.h"
#include "lightGenerator.h"

#include "fs_IDPool.h"

#include "cudaSetup.h"
#include <cuda_gl_interop.h>

#include "cuda_lighting.h"

#include "fs_SSFlags.h"

//******************************
//******************************
//#define COMPLEX_SCENE
//******************************
//******************************

/*
#ifdef _DEBUG
	const char *logLocation = "assimp_log.txt";
#else
	const char *logLocation = "assimp_log.txt";
#endif
*/

#define RANDOM_TEXTURE_PATH "..\\common\\random_scalar.jpg"

/////////////////////////////////////
//// GLOBALS
/////////////////////////////////////
dim3 block(16,8);

float farPlane = 100.0f;
float nearPlane = 1.0f;

int currenttime = 0;
int lastTime = 0;
int timebase = 0;
int frame = 0;
char title[1024];

int mouse_buttons = 0;
int mouse_old_x = 0;
int mouse_old_y = 0;

Camera cam(vec3(20,0,4),
		   normalize(vec3(-1,0,0)),
		   normalize(vec3(0,0,1)));

const float PI = 3.14159f;

int width, height;

int flagComboSelect = 0;
bool displayTile = false;

vector<fs_Object*> *objects;

fs_first_pass_draw_options drawOptions;
mat4 persp;

//float animationStep = 200.0f;
float animationStep = 0.0f;
int startTime = 0;

bool supports41 = false;
bool viewChanged = true;
bool aFrame = false;
bool tilebasedPP = false;

////////// G-BUFFER BINDS ////////////
/////////// FBO Textures /////////////
// Use same depth texture for all FBOs
GLuint depthTexture = 0;

GLuint diffuseTexture = 0;
GLuint normalTexture = 0;
GLuint depthTexture_RGBA = 0;

GLuint miscTextureA = 0;
GLuint miscTextureB = 0;
GLuint miscTexture = 0;

GLuint fbo = 0;
GLuint fboA = 0;
GLuint fboB = 0;

GLuint hdrTexture = 0;
GLuint hdrFBO = 0;

GLuint cudaOutTexture = 0;

GLuint postProcessFBO = 0;
GLuint edgeTexture = 0;
GLuint largeHorizontalBlurTexture = 0;
GLuint smallHorizontalBlurTexture = 0;

GLuint utilityFBO_one = 0;
GLuint utilityFBO_one_texture = 0;

GLuint randomTexture = 0;

int deferredDisplayType = 1;

/////// SHADER PROGRAM BINDS ////////
struct {
	fs_first_pass_shaders point;
	fs_first_pass_shaders ambient;
	fs_first_pass_shaders directional;
	fs_first_pass_shaders spot;
} forward_shaders;

struct {
	fs_first_pass_shaders gBuffer;
	fs_shader_container viewGBuffer;
	fs_shader_container ambient;
	fs_shader_container point;
	fs_shader_container directional;
	fs_shader_container spot;
} deferred_shaders;

fs_shader_container hdrTextureShader;
fs_shader_container cudaTextureShader;
fs_shader_container postProcessSetupShader;
fs_shader_container postProcessShaders[32];
fs_shader_container postProcessShader;

fs_shader_container whiteShader;

struct {
	GLint fb0;
	GLint fb1;
	GLint fb2;
	GLint fb3;
} frame_buffers;

struct {
	unsigned int vertex_array;
	unsigned int vbo_indices;
	unsigned int num_indices;
	unsigned int vbo_data;
} screen_quad_VBO;

const int flagComboSize = 5;
const int flagCombos[5] = {
	SS_FLAG_EDGE,
	SS_FLAG_ANIMATED,
	SS_FLAG_NOT_SKY,
	SS_FLAG_DEPTH,
	SS_FLAG_MAX_DEPTH
};

namespace G_DISPLAY {
	enum {
		LIGHTING = 1,
		NORMAL = 2,
		DIFFUSE = 3,
		SS_VEL = 4,
		DEPTH = 5,
		IDS = 6,
		PACKED_NORMAL = 7,
		CUDA = 8
	};
}

/////////////////////////////////////
void displayPostProcess(GLuint currentOutputTexture);

void initFBO(int width, int height, GLuint &fbo, GLuint &miscTexture);
void destroyFBO(GLuint *fbo, GLuint *miscTexture);

void initHDR_FBO(int width, int height);
void destroyHDR_FBO();
void displayHDR_texture();
void bindHDR_FBO();
void unbindHDR_FBO();

void initPostProcess_FBO(int width, int height);
void destroyPostProcess_FBO();
void bindPostProcess_FBO();
void unbindPostProcss_FBO();

void displayCUDA_texture();
void initCUDA_Texture(int width, int height);
void destroyCUDA_Texture();

void updateScreenSizeUniforms();

void detectEdges();

/////////////////////////////////////
// DISPLAY PROTOTYPE
/////////////////////////////////////
void display(void);


/////////////////////////////////////
// FUNCTIONS
/////////////////////////////////////

void deleteObjects(void) {
	while(!objects->empty()) {
		delete (objects->back());
		objects->back() = NULL;
		objects->pop_back();
	}

	delete objects;
	fs_TexturePool::Destroy();
	ds_LightPool::Destroy(true);
	fs_IDPool::Destroy();
	cul_deleteCreatedArrays();

#ifdef _DEBUG
	_CrtDumpMemoryLeaks();
#endif
}	

void setupAssimpLogging() {
	// Log file normal file parsing status to the console - DIDN'T WORK
	//Assimp::DefaultLogger::create("",Assimp::Logger::NORMAL,aiDefaultLogStream_STDOUT);

	// Log debug-level parsing status to a file
	Assimp::DefaultLogger::create("assimp_log.txt",Assimp::Logger::VERBOSE,aiDefaultLogStream_FILE);

	Assimp::DefaultLogger::get()->info("Testing logging.");
}

void stopAssimpLogging() {
	Assimp::DefaultLogger::kill();
}

const char* dDisplayType() {
	switch(deferredDisplayType) {
	case(G_DISPLAY::NORMAL):
		return " | Displaying Normals";
	case(G_DISPLAY::DIFFUSE):
		return " | Displaying Diffuse";
	case(G_DISPLAY::SS_VEL):
		return " | Displaying SS Velocity";
	case(G_DISPLAY::DEPTH):
		return " | Displaying Depth";
	case(G_DISPLAY::IDS):
		return " | Displaying IDs";
	case(G_DISPLAY::CUDA):
		return " | Tile-based Shading";
	default:
		return " | Normal Shading";
	}
}

void updateAnimation() {
	float step = animationStep * (glutGet(GLUT_ELAPSED_TIME) - startTime)/1000.0f;
	if(animationStep != 0.0f) {
		objects->at(0)->animate(step, 10.0f);
		objects->at(3)->animate(-step, 11.0f);
	}
	// Set frame time uniforms
	int time = glutGet(GLUT_ELAPSED_TIME);
	float timeChange = (time - lastTime)/1000.0f;

	glUseProgram(drawOptions.sC.txShader.shader);
	glUniform1f(drawOptions.sC.txShader.uniforms[U_FRAME_TIME], timeChange);
	glUseProgram(drawOptions.sC.vtShader.shader);
	glUniform1f(drawOptions.sC.vtShader.uniforms[U_FRAME_TIME], timeChange);

	lastTime = time;
}

void updateTitle() {

    //calculate the frames per second
	++frame;

	//get the current time
	currenttime = glutGet(GLUT_ELAPSED_TIME);
	
	//check if a second has passed
	if (currenttime - timebase > 5000) 
	{
		sprintf(title, "Tile-based Deferred Shading | %s%s%s | FPS: %4.2f ",
			(drawOptions.rType == RT_FORWARD ? "Forward Render Mode" : "Deferred Shading Mode"), (drawOptions.rType == RT_FORWARD ? "" : dDisplayType()),
		(drawOptions.rType == RT_DEFERRED && tilebasedPP ? " | Tile-based Post Processing" : ""),
			frame*1000.0/(currenttime-timebase));
		glutSetWindowTitle(title);
	 	timebase = currenttime;		
		frame = 0;
	}
}

// Using code from homework - is array of structures faster data access?
typedef struct {
	glm::vec3 pt;
	glm::vec2 texcoord;
} vertex2_t;

void genScreenQuadVBO() {
	vertex2_t verts [] = { {vec3(-1,1,0),vec2(0,1)},
	{vec3(-1,-1,0),vec2(0,0)},
	{vec3(1,-1,0),vec2(1,0)},
	{vec3(1,1,0),vec2(1,1)}};

	unsigned short indices[] = { 0,1,2,0,2,3};

	//Allocate vertex array
	//Vertex arrays encapsulate a set of generic vertex attributes and the buffers they are bound too
	//Different vertex array per mesh.
	glGenVertexArrays(1, &(screen_quad_VBO.vertex_array));
    glBindVertexArray(screen_quad_VBO.vertex_array);

    
	//Allocate vbos for data
	glGenBuffers(1,&(screen_quad_VBO.vbo_data));
	glGenBuffers(1,&(screen_quad_VBO.vbo_indices));
    
	//Upload vertex data
	glBindBuffer(GL_ARRAY_BUFFER, screen_quad_VBO.vbo_data);
	glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    //Use of strided data, Array of Structures instead of Structures of Arrays
	glVertexAttribPointer(quad_attributes::POSITION, 3, GL_FLOAT, GL_FALSE,sizeof(vertex2_t),0);
	glVertexAttribPointer(quad_attributes::TEXCOORD, 2, GL_FLOAT, GL_FALSE,sizeof(vertex2_t),(void*)sizeof(vec3));
	glEnableVertexAttribArray(quad_attributes::POSITION);
	glEnableVertexAttribArray(quad_attributes::TEXCOORD);

    //indices
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6*sizeof(GLushort), indices, GL_STATIC_DRAW);
    screen_quad_VBO.num_indices = 6;
	//Unplug Vertex Array
    glBindVertexArray(0);
}

void reshape(int w, int h)
{
    width = w;
    height = h;
    glBindFramebuffer(GL_FRAMEBUFFER,0);
	glViewport(0,0,(GLsizei)w,(GLsizei)h);

	// Fix the perspective matrix to retain correct aspect ratio
	persp = perspective(45.0f, 1.0f*w/h, nearPlane, farPlane);
	ds_LightPool::Instance()->setInversePerspective(persp);

	unregisterCUDATextures();
	if (fboA != 0 || depthTexture != 0 || normalTexture != 0 || diffuseTexture != 0 || miscTextureA != 0) {
		destroyFBO(&fboA, &miscTextureA);
	}
	if (fboB != 0 || depthTexture != 0 || normalTexture != 0 || diffuseTexture != 0 || miscTextureB != 0) {
		destroyFBO(&fboB, &miscTextureB);
	}
	if(hdrFBO != 0 || hdrTexture != 0)
		destroyHDR_FBO();
	if(cudaOutTexture != 0)
		destroyCUDA_Texture();
	if(postProcessFBO != 0 || edgeTexture != 0 || largeHorizontalBlurTexture != 0 || smallHorizontalBlurTexture != 0)
		destroyPostProcess_FBO();

	// Regular FBO MUST be created first; it creates the depth texture for other FBOs
	initFBO(w,h, fboA, miscTextureA);
	initFBO(w,h, fboB, miscTextureB);
	initHDR_FBO(w,h);
	initPostProcess_FBO(w,h);
	initCUDA_Texture(w,h);
	registerCUDATextures(depthTexture_RGBA, normalTexture, miscTextureA, miscTextureB, diffuseTexture, cudaOutTexture, edgeTexture);

	glScissor(0,0,width,height);
	viewChanged = true;
}


void mouse(int button, int state, int x, int y)
{
    if (state == GLUT_DOWN) {
        mouse_buttons |= 1<<button;
    } else if (state == GLUT_UP) {
        mouse_buttons = 0;
    }
 
    mouse_old_x = x;
    mouse_old_y = y;
}

void motion(int x, int y)
{
    float dx, dy;
    dx = (float)(x - mouse_old_x);
    dy = (float)(y - mouse_old_y);
 
    if (mouse_buttons & 1<<GLUT_RIGHT_BUTTON) {
		//cam.adjust(0,0,dx,0,0,0);;
    }
    else {
        //cam.adjust(-dx*0.2f,-dy*0.2f,0,0,0,0);
		cam.rotateRight(.25f * dx);
		cam.rotateUp(-.25f * dy);
		viewChanged = true;
    }
 
    mouse_old_x = x;
    mouse_old_y = y;
}

void keyboard(unsigned char key, int x, int y) {
    float tx = 0;
    float tz = 0;
	switch(key) {
	case('w'):
      cam.moveForward(.25f);
	  viewChanged = true;
	  break;
	case('s'):
      cam.moveBackward(.25f);
	  viewChanged = true;
	  break;
	case('d'):
      cam.strafeRight(.25f);
	  viewChanged = true;
	  break;
	case('a'):
      cam.strafeLeft(.25f);
	  viewChanged = true;
	  break;
	case(' '):
		cam.moveUp(.25f);
		viewChanged = true;
		break;
	case('t'):
		if(drawOptions.rType == RT_DEFERRED) {
			drawOptions.rType = RT_FORWARD;
			setForwardRenderer(drawOptions);
		}
		else if(drawOptions.rType == RT_FORWARD) {
			drawOptions.rType = RT_DEFERRED;
			setDeferredRenderer(drawOptions);
		}
		// Cause the FBO's to get rebuilt
		reshape(width, height);
		break;
	case('y'):
		tilebasedPP = !tilebasedPP;
		break;
	case('i'):
		if(flagComboSelect < flagComboSize - 1)
			++flagComboSelect;
		else
			flagComboSelect = 0;
		break;
	case('u'):
		displayTile = !displayTile;
		break;
	case('1'):
		if(deferredDisplayType == G_DISPLAY::LIGHTING)
			deferredDisplayType = G_DISPLAY::CUDA;
		else
			deferredDisplayType = G_DISPLAY::LIGHTING;
		break;
	case('2'): {
		if(deferredDisplayType == G_DISPLAY::NORMAL)
			deferredDisplayType = G_DISPLAY::PACKED_NORMAL;
		else
			deferredDisplayType = G_DISPLAY::NORMAL;
			   }
		break;
	case('3'):
		deferredDisplayType = G_DISPLAY::DIFFUSE;
		break;
	case('4'):
		deferredDisplayType = G_DISPLAY::SS_VEL;
		break;
	case('5'):
		deferredDisplayType = G_DISPLAY::DEPTH;
		break;
	case('6'):
		deferredDisplayType = G_DISPLAY::IDS;
		break;
	}

	// Can't hurt to always change this
	changeGBufferDisplayType(deferred_shaders.viewGBuffer, deferredDisplayType);
}

void init() {
	glEnable(GL_DEPTH_TEST);
    glClearColor(0.0f, 0.0f, 0.0f,1.0f);

	// Disable vsync
	//if(WGLEW_EXT_swap_control)
		wglSwapIntervalEXT(6);
}

void setupGLUT(int argc, char *argv[]) {
	glutInit(&argc, argv);
	glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    width = 800;
    height = 600;
	glutInitWindowSize(width,height);
	glutCreateWindow("Tile-based Deferred Shading | Initializing");
	glewInit();
	GLenum err = glewInit();
	if (GLEW_OK != err)
	{
		/* Problem: glewInit failed, something is seriously wrong. */
		cout << "glewInit failed, aborting." << endl;
		exit (1);
	}
	cout << "Status: Using GLEW " << glewGetString(GLEW_VERSION) << endl;
	cout << "OpenGL version " << glGetString(GL_VERSION) << " supported" << endl;

	// Disable vsync
	if(WGLEW_EXT_swap_control)
		wglSwapIntervalEXT(0);
}

void initShaders() {
	cout << "Loading shaders...\n";

	// Forward
	initForwardAmbient(forward_shaders.ambient);
	initForwardPoint(forward_shaders.point);
	initForwardDirectional(forward_shaders.directional);
	initForwardSpot(forward_shaders.spot);

	// Deferred
	initDeferredGBuffer(deferred_shaders.gBuffer);
	initDeferredViewGBuffer(deferred_shaders.viewGBuffer);
	initDeferredAmbient(deferred_shaders.ambient);
	initDeferredDirectional(deferred_shaders.directional);
	initDeferredPoint(deferred_shaders.point);
	initDeferredSpot(deferred_shaders.spot);

	// HDR
	initHDRTexture(hdrTextureShader);

	// CUDA
	initCUDATexture(cudaTextureShader);

	// White shader
	initWhiteShader(whiteShader);

	// Post process
	cout << "Creating post-process shader permutations...\n";
	initPostProcessSetup(postProcessSetupShader);
	initPostProcessShaders(postProcessShaders);
	postProcessShader = postProcessShaders[23];
	cout << "Shader loading complete.\n";
}

int main(int arcg, char *argv[]) {
	atexit(deleteObjects);

#ifdef _DEBUG
	//_CrtSetBreakAlloc(2569);
#endif

	// OpenGL/CUDA inter initialization
	cudaError_t e = cudaGLSetGLDevice(0);

	setupGLUT(arcg, argv);
	const char *result = (const char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
	if(result)
		supports41 = (result[0] >= '4' && result[2] >= '1');

	// Set initial perspective
	mat4 persp = perspective(45.0f, 1.0f, 0.1f, 100.0f);
	ds_LightPool::Instance()->setInversePerspective(persp);

	// Add the lights
	setupPointLights();
	setupDirectionalLights();
#ifdef COMPLEX_SCENE
	generateRandomPointLights(4093, vec3(-40.0f, -40.0f, -15.0f), vec3(40.0f, 40.0f, 15.0f), .5f);
#else
	generateRandomPointLights(1021, vec3(-20.0f, -20.0f, 1.0f), vec3(20.0f, 20.0f, 15.0f), .75f);
#endif
	//generateRandomSpotLights(64, vec3(-20.0f, -20.0f, 5.0f), vec3(20.0f, 20.0f, 20.0f), .66f, vec3(.0f,.0f, -1.0f), false);
	//ds_LightPool::Instance()->addSpotLight(vec3(7.0f, -4.0f, 10.0f), vec3(.0f, .0f, -1.0f), vec3(1.0f, .0f, .0f), 50.0f);
	//ds_LightPool::Instance()->addSpotLight(vec3(-7.0f, -4.0f, 10.0f), vec3(.0f, 1.0f, 0.0f), vec3(1.0f, .0f, .0f), 40.0f);

	// Log and load model files
	setupAssimpLogging();
	objects = new vector<fs_Object*>();
#ifndef COMPLEX_SCENE
	objects->push_back(new fs_Object("..\\scenes\\models\\spider\\","spider.obj"));
	objects->at(0)->rotate(180, vec3(0,1,0));
	objects->at(0)->rotate(90, vec3(1,0,0));
	objects->at(0)->scale(6.0f);
	objects->at(0)->translate(vec3(0.0f,0.0f,3.0f));
	objects->push_back(new fs_Object(objects->at(0)));
	objects->at(1)->scale(.2f);
	objects->at(1)->translate(vec3(-5.0f,0,8.0f));

	objects->push_back(createStreet());

	
	objects->push_back(new fs_Object("..\\scenes\\models\\","Suzanne_248.blend"));
	
	objects->at(3)->rotate(90, vec3(0,0,1));
	objects->at(3)->rotate(15, vec3(0,1,0));
	objects->at(3)->scale(3.0f);
	objects->at(3)->translate(vec3(7.5f, -3.0f, 4.0f));
	objects->push_back(new fs_Object(objects->at(0)));
	objects->at(4)->translate(vec3(0.0f,0,5.0f));
	objects->push_back(new fs_Object(objects->at(4)));
	objects->at(5)->translate(vec3(0.0f,0,5.0f));
	objects->push_back(new fs_Object(objects->at(5)));
	objects->at(6)->translate(vec3(0.0f,0,5.0f));
	objects->push_back(new fs_Object(objects->at(6)));
	objects->at(7)->translate(vec3(0.0f,0,5.0f));
	objects->push_back(new fs_Object(objects->at(7)));
	objects->at(8)->translate(vec3(0.0f,0,5.0f));
	objects->push_back(new fs_Object(objects->at(8)));
	objects->at(9)->translate(vec3(0.0f,0,5.0f));
#else
	objects->push_back(new fs_Object("..\\scenes\\models\\sponza_obj\\","sponza.obj"));
	objects->at(0)->scale(25.0f);
	objects->at(0)->rotate(180, vec3(0,1,0));
	objects->at(0)->rotate(90, vec3(1,0,0));
	objects->at(0)->rotate(-90, vec3(0,0,1));
	objects->at(0)->translate(vec3(20.0f,5.0f,3.61f));
#endif // #ifndef COMPLEX_SCENE

	stopAssimpLogging();

	// Setup initial view
	mat4 view = cam.getView();
	drawOptions.view = view;

	// Shader initialization
	initShaders();

	// Forward initialization
	initForwardRenderer(drawOptions);
	setForwardRenderer(drawOptions);

	// Load the random texture
	randomTexture = fs_TexturePool::Instance()->loadTexture(RANDOM_TEXTURE_PATH);

	// Deferred Initialization
	//initFBO(width, height); - Done with reshape function which is called when it is set
	genScreenQuadVBO();

	glutDisplayFunc(display);
	glutReshapeFunc(reshape);	
    glutKeyboardFunc(keyboard);
    glutMouseFunc(mouse);
	glutMotionFunc(motion);

#ifdef USE_PROFILER
	drawOptions.rType = RT_DEFERRED;
	deferredDisplayType = G_DISPLAY::CUDA;
#endif

	startTime = glutGet(GLUT_ELAPSED_TIME);
	cout << "Startup Time: " << startTime/1000.0f << "s" << endl;

	glutMainLoop();
	return 0;
}

/////////////////
void forwardRender(void);
void deferredRender(void);

void display(void)
{
	aFrame = !aFrame;
	if(aFrame) {
		fbo = fboA;
		miscTexture = miscTextureA;
	}
	else {
		fbo = fboB;
		miscTexture = miscTextureB;
	}

	// clear the screen
	glClearColor(0.0f,0.0f,0.0f,0.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    updateTitle();

	mat4 view = cam.getView();
	drawOptions.view = view;

	if(drawOptions.rType == RT_FORWARD) {
		forwardRender();
	}
	else if(drawOptions.rType == RT_DEFERRED) {
		deferredRender();
	}

    glutPostRedisplay();
	glutSwapBuffers();

	updateAnimation();
}

void forwardRender() {
	mat4 view = cam.getView();
	drawOptions.view = view;

	bindHDR_FBO();
	glEnable(GL_DEPTH_TEST);

	// Set uniforms for both texture and vertex based shaders
	drawOptions.sC = forward_shaders.ambient;
	setFirstPassViewAndProjection(drawOptions.sC, view, persp);

	// Calculate the view-space up and down vectors and send to openGL
	setUpAndDownVector(drawOptions);
	setTopAndBottomColor(drawOptions);

	ds_LightPool *lp = ds_LightPool::Instance();

	// Ambient pass
	glDepthFunc(GL_LESS);
	for(unsigned int i = 0; i < objects->size(); ++i) {
		if(objects->at(i))
			objects->at(i)->draw(drawOptions);
	}

	// Enable blending
	glDepthFunc(GL_LEQUAL);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);

	// Uncomment to remove coloring from ambient pass
	//glClear(GL_COLOR_BUFFER_BIT);

///*
	// Point pass
	drawOptions.sC = forward_shaders.point;
	setFirstPassViewAndProjection(forward_shaders.point, view, persp);
	ds_point_light_batch plb;
	for(unsigned int j = 0; j < (unsigned int)lp->numPointLightBatches(); ++j) {
		plb = lp->pointLightBatch(j);
		setPointLightPassUniforms(plb, drawOptions);
		for(unsigned int i = 0; i < objects->size(); ++i) {
			if(objects->at(i))
				objects->at(i)->draw(drawOptions);
		}
	}
//*/
	
///*
	// Directional pass
	drawOptions.sC = forward_shaders.directional;
	setFirstPassViewAndProjection(forward_shaders.directional, view, persp);
	ds_directional_light_batch dlb;
	for(unsigned int j = 0; j < (unsigned int)lp->numDirectionalLightBatches(); ++j) {
		dlb = lp->directionalLightBatch(j);
		setDirectionalLightPassUniforms(dlb, drawOptions);
		for(unsigned int i = 0; i < objects->size(); ++i) {
			if(objects->at(i))
				objects->at(i)->draw(drawOptions);
		}
	}
//*/
///*
	// Spot Pass
	drawOptions.sC = forward_shaders.spot;
	setFirstPassViewAndProjection(forward_shaders.spot, view, persp);
	ds_spot_light_batch slb;
	for(unsigned int j = 0; j < (unsigned int)lp->numSpotLightBatches(); ++j) {
		slb = lp->spotLightBatch(j);
		setSpotLightPassUniforms(slb, drawOptions);
		for(unsigned int i = 0; i < objects->size(); ++i) {
			if(objects->at(i))
				objects->at(i)->draw(drawOptions);
		}
	}
//*/
	glDisable(GL_BLEND);
	glBindVertexArray(0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	unbindHDR_FBO();
	displayHDR_texture();
}

void bindGBufferTXs(fs_shader_container s) {
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, depthTexture_RGBA);
	glUniform1i(s.uniforms[U_DEPTH_TX], 0);

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, normalTexture);
	glUniform1i(s.uniforms[U_NORMAL_TX], 1);

	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, diffuseTexture);
	glUniform1i(s.uniforms[U_DIFFUSE_TX], 2);

	glActiveTexture(GL_TEXTURE3);
	glBindTexture(GL_TEXTURE_2D, miscTexture);
	glUniform1i(s.uniforms[U_MISC_TX], 3);
}

void displayGBuffer() {
	glUseProgram(deferred_shaders.viewGBuffer.shader);
	glUniform1i(deferred_shaders.viewGBuffer.uniforms[U_GBUFFER_VIEW], deferredDisplayType);
    glUniform1f(deferred_shaders.viewGBuffer.uniforms[U_FAR], farPlane);
    glUniform1f(deferred_shaders.viewGBuffer.uniforms[U_NEAR], nearPlane);
	glUniform1i(deferred_shaders.viewGBuffer.uniforms[U_SCREEN_WIDTH], width);
	glUniform1i(deferred_shaders.viewGBuffer.uniforms[U_SCREEN_HEIGHT], height);
	glUniformMatrix4fv(deferred_shaders.viewGBuffer.uniforms[U_PERSP], 1, GL_FALSE, &persp[0][0]);

	glDisable(GL_BLEND);

	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);
	
	glEnable(GL_TEXTURE_2D);
	bindGBufferTXs(deferred_shaders.viewGBuffer);

	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

	glBindVertexArray(0);
}

void setDLUniforms(fs_shader_container sC) {
	glUseProgram(sC.shader);
	glUniform1i(sC.uniforms[U_GBUFFER_VIEW], deferredDisplayType);
    glUniform1f(sC.uniforms[U_FAR], farPlane);
    glUniform1f(sC.uniforms[U_NEAR], nearPlane);
	//glUniform1i(sC.uniforms[U_SCREEN_WIDTH], width);
	//glUniform1i(sC.uniforms[U_SCREEN_HEIGHT], height);
	glUniformMatrix4fv(sC.uniforms[U_PERSP], 1, GL_FALSE, &persp[0][0]);

	glUniformMatrix4fv(sC.uniforms[U_VIEW], 1, GL_FALSE, &(cam.getView()[0][0]));

	mat4 invPersp = inverse(persp);
	glUniformMatrix4fv(sC.uniforms[U_INV_PERSP], 1, GL_FALSE, &invPersp[0][0]);
}

void calculateCUDALighting() {
	mat4 view = cam.getView();
	
	// Make sure nothing is bound
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);

	ds_point_light_device pld = ds_LightPool::Instance()->pointLightCudaPointers(&persp, width, height, &cam, viewChanged);

	performCUDALighting(block, width, height, pld, aFrame);

	bindHDR_FBO();
	glClear(GL_COLOR_BUFFER_BIT);
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);

	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);

	setUpAndDownVector(deferred_shaders.ambient, view);
	setTopAndBottomColor(deferred_shaders.ambient);

// Ambient pass
///*
	setDLUniforms(deferred_shaders.ambient);
	bindGBufferTXs(deferred_shaders.ambient);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

//*/
///*
	// Directional pass
	setDLUniforms(deferred_shaders.directional);
	bindGBufferTXs(deferred_shaders.directional);
	ds_directional_light_batch dlb;
	ds_LightPool *lp = ds_LightPool::Instance();
	for(unsigned int j = 0; j < (unsigned int)lp->numDirectionalLightBatches(); ++j) {
		dlb = lp->directionalLightBatch(j);
		setDirectionalLightPassUniforms(&dlb, &(deferred_shaders.directional), &view);
		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}
//*/

	// Point pass - USE accumulation from CUDA
///*
	glUseProgram(cudaTextureShader.shader);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, cudaOutTexture);
	glUniform1i(cudaTextureShader.uniforms[U_CUDA_TEXTURE], 0);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
///*/
///*
	// Spot Pass
	setDLUniforms(deferred_shaders.spot);
	bindGBufferTXs(deferred_shaders.spot);
	ds_spot_light* sl;
	mat3 invTrans = transpose(inverse(mat3(view)));
	glEnable(GL_SCISSOR_TEST);
	for(unsigned int j = 0; j < (unsigned int)lp->numSpotLights(); ++j) {
		sl = lp->spotLight(j);
		updateScissorRect(sl, &view, &persp, &invTrans, width, height);
		setSpotLightUniforms(sl, &(deferred_shaders.spot), &view);
		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}


//*/
	glDisable(GL_SCISSOR_TEST);
	glDisable(GL_BLEND);

	unbindHDR_FBO();

	viewChanged = false;
}

void calculateDeferredLighting() {
	mat4 view = cam.getView();

	setUpAndDownVector(deferred_shaders.ambient, view);
	setTopAndBottomColor(deferred_shaders.ambient);

	bindHDR_FBO();

	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);

	glDisable(GL_DEPTH_TEST);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_BLEND);
	glBlendFunc(GL_ONE, GL_ONE);

	ds_LightPool* lp = ds_LightPool::Instance();

	mat3 invTrans = transpose(inverse(mat3(view)));



	// Ambient pass
///*
	setDLUniforms(deferred_shaders.ambient);
	bindGBufferTXs(deferred_shaders.ambient);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

//*/
///*
	// Directional pass
	setDLUniforms(deferred_shaders.directional);
	bindGBufferTXs(deferred_shaders.directional);
	ds_directional_light_batch dlb;
	for(unsigned int j = 0; j < (unsigned int)lp->numDirectionalLightBatches(); ++j) {
		dlb = lp->directionalLightBatch(j);
		setDirectionalLightPassUniforms(&dlb, &(deferred_shaders.directional), &view);
		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}
//*/

	glEnable(GL_SCISSOR_TEST);

	// Point pass
///*
	setDLUniforms(deferred_shaders.point);
	bindGBufferTXs(deferred_shaders.point);
	ds_point_light* pl;
	for(unsigned int j = 0; j < (unsigned int)lp->numPointLights(); ++j) {
		pl = lp->pointLight(j);
		setPointLightPassUniforms(pl, &(deferred_shaders.point));
		updateScissorRect(pl, &view, &persp, &invTrans, width, height, &cam);
		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}
///*/
///*
	// Spot Pass
	setDLUniforms(deferred_shaders.spot);
	bindGBufferTXs(deferred_shaders.spot);
	ds_spot_light* sl;
	for(unsigned int j = 0; j < (unsigned int)lp->numSpotLights(); ++j) {
		sl = lp->spotLight(j);
		updateScissorRect(sl, &view, &persp, &invTrans, width, height);
		setSpotLightUniforms(sl, &(deferred_shaders.spot), &view);
		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}


//*/
	glDisable(GL_SCISSOR_TEST);
	glDisable(GL_BLEND);

	unbindHDR_FBO();
}

void deferredRender() {
	mat4 view = cam.getView();

	// Prepare the FBO for output
	glDisable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);
	glEnable(GL_DEPTH_TEST);
	glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

	drawOptions.sC = deferred_shaders.gBuffer;
	glActiveTexture(GL_TEXTURE0);
	setFirstPassViewAndProjection(drawOptions.sC, view, persp);
	for(unsigned int i = 0; i < objects->size(); ++i)
		if(objects->at(i))
			objects->at(i)->draw(drawOptions);

	// Unbind framebuffer
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D,0); 
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    glDisable(GL_DEPTH_TEST);
	glClear(GL_COLOR_BUFFER_BIT);
	
	if(deferredDisplayType == G_DISPLAY::CUDA) {
		calculateCUDALighting();
		//displayHDR_texture();
		displayPostProcess(hdrTexture);
	}
	else if(deferredDisplayType != G_DISPLAY::LIGHTING)
		displayGBuffer();
	else {
		calculateDeferredLighting();
		//displayHDR_texture();
		displayPostProcess(hdrTexture);
	}
}

void destroyFBO(GLuint *fbo, GLuint *miscTexture) {
	glDeleteTextures(1, &diffuseTexture);
	diffuseTexture = 0;
	glDeleteTextures(1, &normalTexture);
	normalTexture = 0;
	glDeleteTextures(1, &depthTexture);
	depthTexture = 0;
	glDeleteTextures(1, &depthTexture_RGBA);
	depthTexture_RGBA = 0;
	glDeleteFramebuffers(1, fbo);
	*fbo = 0;
	glDeleteTextures(1, miscTexture);
	*miscTexture = 0;
	// Unregister the CUDA resources
}

void destroyHDR_FBO() {
	glDeleteTextures(1, &hdrTexture);
	glDeleteFramebuffers(1, &hdrFBO);
}

void destroyPostProcess_FBO() {
	glDeleteTextures(1, &edgeTexture);
	glDeleteTextures(1, &smallHorizontalBlurTexture);
	glDeleteTextures(1, &largeHorizontalBlurTexture);
	glDeleteFramebuffers(1, &postProcessFBO);
}

void destroyCUDA_Texture() {
	glDeleteTextures(1, &cudaOutTexture);
}

void initCUDA_Texture(int w, int h) {
	glActiveTexture(GL_TEXTURE0);
	glGenTextures(1, &cudaOutTexture);

	// Create the CUDA output texture
	glBindTexture(GL_TEXTURE_2D, cudaOutTexture);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_INT, 0);

	glBindTexture(GL_TEXTURE_2D, 0);
}

void initPostProcess_FBO(int w, int h) {
	GLenum FBOstatus;

	glActiveTexture(GL_TEXTURE0);
	glGenTextures(1, &edgeTexture);
	glGenTextures(1, &largeHorizontalBlurTexture);
	glGenTextures(1, &smallHorizontalBlurTexture);

	// Edge texture
	glBindTexture(GL_TEXTURE_2D, edgeTexture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
	glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_UNSIGNED_INT,0);

	// Large horizontal blur texture
	glBindTexture(GL_TEXTURE_2D, largeHorizontalBlurTexture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
	glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_FLOAT,0);

	// Small horizontal blur texture
	glBindTexture(GL_TEXTURE_2D, smallHorizontalBlurTexture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
	glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,w,h,0,GL_RGBA,GL_FLOAT,0);

	glGenFramebuffers(1, &postProcessFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, postProcessFBO);

	// Instruct openGL that we won't bind a color texture with the currently binded FBO
	glReadBuffer(GL_NONE);

	// Enable writing through the output varaibles (not competely sure what this does)
    GLenum draws [3];
    draws[0] = GL_COLOR_ATTACHMENT0;
    draws[1] = GL_COLOR_ATTACHMENT1;
	draws[2] = GL_COLOR_ATTACHMENT2;
	glDrawBuffers(3, draws);

	// Attach the textures
	glBindTexture(GL_TEXTURE_2D, edgeTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, edgeTexture, 0);
	glBindTexture(GL_TEXTURE_2D, largeHorizontalBlurTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, largeHorizontalBlurTexture, 0);
	glBindTexture(GL_TEXTURE_2D, smallHorizontalBlurTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, smallHorizontalBlurTexture, 0);

	// check FBO status
	FBOstatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if(FBOstatus != GL_FRAMEBUFFER_COMPLETE) {
		printf("GL_FRAMEBUFFER_COMPLETE failed, CANNOT use postProcessFBO\n");
        checkFramebufferStatus(FBOstatus);
	}

	// switch back to window-system-provided framebuffer
	glClear(GL_DEPTH_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);	
}



void initHDR_FBO(int w, int h) {
	GLenum FBOstatus;

	glActiveTexture(GL_TEXTURE0);
	glGenTextures(1, &hdrTexture);

	// Create the HDR texture
	glBindTexture(GL_TEXTURE_2D, hdrTexture);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, w, h, 0, GL_RGBA, GL_FLOAT, 0);

	// create a framebuffer object
	glGenFramebuffers(1, &hdrFBO);
	glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);

	glReadBuffer(GL_NONE);

	GLenum draw[]  = {GL_COLOR_ATTACHMENT0};
	glDrawBuffers(1, draw);

	if(drawOptions.rType == RT_FORWARD) {
		glBindTexture(GL_TEXTURE_2D, depthTexture);
		glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0);
	}

	// attach the textures to the color attachment points
	glBindTexture(GL_TEXTURE_2D, hdrTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, hdrTexture, 0);

	// check FBO status
	FBOstatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if(FBOstatus != GL_FRAMEBUFFER_COMPLETE) {
		printf("initHDR_FBO: GL_FRAMEBUFFER_COMPLETE failed, CANNOT use FBO\n");
        checkFramebufferStatus(FBOstatus);
	}

	// switch back to window-system-provided framebuffer
	glClear(GL_DEPTH_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);
}

void initFBO(int w, int h, GLuint &fbo, GLuint &miscTexture) {
	GLenum FBOstatus;
	
	glActiveTexture(GL_TEXTURE0);

	// Create the depth texture for z-test purposes
	if(depthTexture == 0) {
		glGenTextures(1, &depthTexture);
		glBindTexture(GL_TEXTURE_2D, depthTexture);
		
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);

		glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
	}


	// Create the RGBA depth texture for use by cuda
	if(depthTexture_RGBA == 0) {
		glGenTextures(1, &depthTexture_RGBA);
		glBindTexture(GL_TEXTURE_2D, depthTexture_RGBA);
		
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
		glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);

		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_INT, 0);
	}


	// Create the normal texture
	if(normalTexture == 0) {
		glGenTextures(1, &normalTexture);
		glBindTexture(GL_TEXTURE_2D, normalTexture);
		
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);

		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_INT, 0);
	}

	// Diffuse color
	if(diffuseTexture == 0) {
		glGenTextures(1, &diffuseTexture);
		glBindTexture(GL_TEXTURE_2D, diffuseTexture);
	
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_INT,0);
	}

	// Miscellaneous (SS velocity, material ID, object ID)
	if(miscTexture == 0) {
		glGenTextures(1, &miscTexture);
		glBindTexture(GL_TEXTURE_2D, miscTexture);
		
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
		
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_INT,0);
	}
	// create a framebuffer object
	glGenFramebuffers(1, &fbo);
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);
	
	// Instruct openGL that we won't bind a color texture with the currently binded FBO
	glReadBuffer(GL_NONE);

	// Enable writing through the output varaibles (not competely sure what this does)
    GLenum draws [4];
    draws[0] = GL_COLOR_ATTACHMENT0;
    draws[1] = GL_COLOR_ATTACHMENT1;
	draws[2] = GL_COLOR_ATTACHMENT2;
	draws[3] = GL_COLOR_ATTACHMENT3;
	glDrawBuffers(4, draws);

	// attach the texture to FBO depth attachment point
	glBindTexture(GL_TEXTURE_2D, depthTexture);
	glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, depthTexture, 0);
	
	// attach the textures to the color attachment points
	glBindTexture(GL_TEXTURE_2D, normalTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, normalTexture, 0);
	glBindTexture(GL_TEXTURE_2D, diffuseTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, diffuseTexture, 0);
	glBindTexture(GL_TEXTURE_2D, miscTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, miscTexture, 0);
	glBindTexture(GL_TEXTURE_2D, depthTexture);    
	glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT3, depthTexture_RGBA, 0);

	// check FBO status
	FBOstatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if(FBOstatus != GL_FRAMEBUFFER_COMPLETE) {
		printf("GL_FRAMEBUFFER_COMPLETE failed, CANNOT use FBO\n");
        checkFramebufferStatus(FBOstatus);
	}

	// switch back to window-system-provided framebuffer
	glClear(GL_DEPTH_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);	
}

void updateScreenSizeUniforms(fs_shader_container &shader) {
	glUseProgram(shader.shader);
	glUniform1i(shader.uniforms[U_SCREEN_WIDTH], (GLint)width);
	glUniform1i(shader.uniforms[U_SCREEN_HEIGHT], (GLint)height);
}

void unbindHDR_FBO() {
	// Unbind the FBO
	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D,0); 
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void bindHDR_FBO() {
	// Prepare the HDR FBO for output
	glDisable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
	glEnable(GL_DEPTH_TEST);
	glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
}

void displayCUDA_texture() {
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, cudaOutTexture);
	glDisable(GL_BLEND | GL_SCISSOR_TEST | GL_DEPTH_TEST);

	glClear(GL_COLOR_BUFFER_BIT);

	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);

	glUseProgram(cudaTextureShader.shader);
	glUniform1i(cudaTextureShader.uniforms[U_CUDA_TEXTURE], 0);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

	glBindVertexArray(0);
}

void displayHDR_texture() {
	// Make sure no frame-buffer is bound
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, hdrTexture);
	glUniform1i(hdrTextureShader.uniforms[U_HDR_TEXTURE], 0);
	glDisable(GL_BLEND | GL_SCISSOR_TEST | GL_DEPTH_TEST);

	glClear(GL_COLOR_BUFFER_BIT);

	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);

	glUseProgram(hdrTextureShader.shader);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

	glBindVertexArray(0);
}

void detectEdges() {
	// Bind the HDR fbo for output - FOR NOW
	//bindHDR_FBO();
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	glEnable(GL_BLEND);
	glDisable(GL_DEPTH_TEST);
	glBlendFunc(GL_ONE, GL_ONE);

	glUseProgram(postProcessSetupShader.shader);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, depthTexture);
	glUniform1i(postProcessSetupShader.uniforms[U_DEPTH_TX], 0);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, normalTexture);
	glUniform1i(postProcessSetupShader.uniforms[U_NORMAL_TX], 1);
	glUniform1f(postProcessSetupShader.uniforms[U_FAR], farPlane);
	glUniform1f(postProcessSetupShader.uniforms[U_NEAR], nearPlane);

	// Draw the full-screen quad
	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

	glDisable(GL_BLEND);
	glEnable(GL_DEPTH_TEST);

	// Unbind the FBO
	//unbindHDR_FBO();
}

void tilebasedPostProcess(GLuint currentOutputTexture) {
	unsigned int *numTiles;
	int *shaderIDs;
	int blocksPerRow = getCUDAShaderIDs(&numTiles, &shaderIDs, block, width, height, aFrame, nearPlane, farPlane);

	// Bind the full-screen quad
	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);

	// Set the active textures
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, currentOutputTexture);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, edgeTexture);
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, largeHorizontalBlurTexture);
	glActiveTexture(GL_TEXTURE3);
	glBindTexture(GL_TEXTURE_2D, smallHorizontalBlurTexture);
	if(aFrame) {
		glActiveTexture(GL_TEXTURE4);
		glBindTexture(GL_TEXTURE_2D, miscTextureA);
		glActiveTexture(GL_TEXTURE5);
		glBindTexture(GL_TEXTURE_2D, miscTextureB);
	}
	else {
		glActiveTexture(GL_TEXTURE4);
		glBindTexture(GL_TEXTURE_2D, miscTextureB);
		glActiveTexture(GL_TEXTURE5);
		glBindTexture(GL_TEXTURE_2D, miscTextureA);
	}
	glActiveTexture(GL_TEXTURE6);
	glBindTexture(GL_TEXTURE_2D, normalTexture);
	glActiveTexture(GL_TEXTURE7);
	glBindTexture(GL_TEXTURE_2D, depthTexture);
	glActiveTexture(GL_TEXTURE8);
	glBindTexture(GL_TEXTURE_2D, randomTexture);

	mat4 invPersp = inverse(persp);

	glEnable(GL_SCISSOR_TEST);

	bool loadedUniforms;
	fs_shader_container cShader;
	int x, y;
/*
	for(int i = 0; i < 32; ++i) {
		cShader = postProcessShaders[i];
		glUseProgram(cShader.shader);
		glUniform1i(cShader.uniforms[U_OUT_TX], 0);
		glUniform1i(cShader.uniforms[U_EDGE_TX], 1);
		glUniform1i(cShader.uniforms[U_LARGE_HORIZONTAL_BLUR_TX],2);
		glUniform1i(cShader.uniforms[U_SMALL_HORIZONTAL_BLUR_TX],3);
		glUniform1i(cShader.uniforms[U_MISC_TX],4);
		glUniform1i(cShader.uniforms[U_LAST_MISC_TX],5);
		glUniform1i(cShader.uniforms[U_NORMAL_TX],6);
		glUniform1i(cShader.uniforms[U_DEPTH_TX],7);
		glUniform1i(cShader.uniforms[U_RANDOM_TX],8);

		glUniformMatrix4fv(cShader.uniforms[U_INV_PERSP], 1, GL_FALSE, &invPersp[0][0]);
		
		glUniform1i(cShader.uniforms[U_SCREEN_WIDTH], width);
		glUniform1i(cShader.uniforms[U_SCREEN_HEIGHT], height);
		glUniform1f(cShader.uniforms[U_FAR], farPlane);
		glUniform1f(cShader.uniforms[U_NEAR], nearPlane);
	}

	for(unsigned int i = 0; i < *numTiles; ++i) {
		glUseProgram(postProcessShaders[shaderIDs[i]].shader);

		// Define the scissor rect
		x = block.x*(i % blocksPerRow);
		y = block.y*(i / blocksPerRow);
		glScissor(x, y, block.x, block.y);

		if(displayTile) {
			if(flagComboSelect == 3) {
				if(((SS_FLAG_DEPTH & i) != 0) && ((SS_FLAG_MAX_DEPTH & shaderIDs[i]) == 0)) 
					glUseProgram(whiteShader.shader);
			}
			else {
				if((flagCombos[flagComboSelect] & shaderIDs[i]) != 0)
					glUseProgram(whiteShader.shader);
			}
		}

		// Draw the full-screen quad
		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}
*/

	for(int i = 0; i < 32; ++i) {
		loadedUniforms = false;
		cShader = postProcessShaders[i];

		for(unsigned int j = 0; j < *numTiles; ++j) {
			int s = shaderIDs[j];

			if(shaderIDs[j] == i) {
				if(!loadedUniforms) {
					glUseProgram(cShader.shader);
					glUniform1i(cShader.uniforms[U_OUT_TX], 0);
					glUniform1i(cShader.uniforms[U_EDGE_TX], 1);
					glUniform1i(cShader.uniforms[U_LARGE_HORIZONTAL_BLUR_TX],2);
					glUniform1i(cShader.uniforms[U_SMALL_HORIZONTAL_BLUR_TX],3);
					glUniform1i(cShader.uniforms[U_MISC_TX],4);
					glUniform1i(cShader.uniforms[U_LAST_MISC_TX],5);
					glUniform1i(cShader.uniforms[U_NORMAL_TX],6);
					glUniform1i(cShader.uniforms[U_DEPTH_TX],7);
					glUniform1i(cShader.uniforms[U_RANDOM_TX],8);

					glUniformMatrix4fv(cShader.uniforms[U_INV_PERSP], 1, GL_FALSE, &invPersp[0][0]);
					
					glUniform1i(cShader.uniforms[U_SCREEN_WIDTH], width);
					glUniform1i(cShader.uniforms[U_SCREEN_HEIGHT], height);
					glUniform1f(cShader.uniforms[U_FAR], farPlane);
					glUniform1f(cShader.uniforms[U_NEAR], nearPlane);

					loadedUniforms = true;
				}

				// Define the scissor rect
				x = block.x*(j % blocksPerRow);
				y = block.y*(j / blocksPerRow);
				glScissor(x, y, block.x, block.y);

				if(displayTile) {
					if(flagComboSelect == 3) {
						if(((SS_FLAG_DEPTH & i) != 0) && ((SS_FLAG_MAX_DEPTH & i) == 0)) 
							glUseProgram(whiteShader.shader);
					}
					else {
						if((flagCombos[flagComboSelect] & i) != 0)
							glUseProgram(whiteShader.shader);
					}
				}

				// Draw the full-screen quad
				glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

				if(displayTile)
					glUseProgram(cShader.shader);
			}
		}
	}

	glDisable(GL_SCISSOR_TEST);
}

void displayPostProcess(GLuint currentOutputTexture) {
	// Prepare for post-processing
	glBindFramebuffer(GL_FRAMEBUFFER, postProcessFBO);

	glDisable(GL_DEPTH_TEST);

	glUseProgram(postProcessSetupShader.shader);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, depthTexture);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, normalTexture);
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, hdrTexture);

	glUniform1i(postProcessSetupShader.uniforms[U_DEPTH_TX], 0);
	glUniform1i(postProcessSetupShader.uniforms[U_NORMAL_TX], 1);
	glUniform1i(postProcessSetupShader.uniforms[U_OUT_TX],2);

	glUniform1f(postProcessSetupShader.uniforms[U_FAR], farPlane);
	glUniform1f(postProcessSetupShader.uniforms[U_NEAR], nearPlane);
	glUniform1i(postProcessSetupShader.uniforms[U_SCREEN_WIDTH], width);
	glUniform1i(postProcessSetupShader.uniforms[U_SCREEN_HEIGHT], height);

	// Draw the full-screen quad
	glBindVertexArray(screen_quad_VBO.vertex_array);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, screen_quad_VBO.vbo_indices);
	glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);

	// Perform post processing
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glDisable(GL_BLEND | GL_SCISSOR_TEST | GL_DEPTH_TEST);

	glClear(GL_COLOR_BUFFER_BIT);

	if(tilebasedPP)
		tilebasedPostProcess(currentOutputTexture);
	else {
		// Bind necessary textures
		glUseProgram(postProcessShader.shader);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, currentOutputTexture);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, edgeTexture);
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, largeHorizontalBlurTexture);
		glActiveTexture(GL_TEXTURE3);
		glBindTexture(GL_TEXTURE_2D, smallHorizontalBlurTexture);
		if(aFrame) {
			glActiveTexture(GL_TEXTURE4);
			glBindTexture(GL_TEXTURE_2D, miscTextureA);
			glActiveTexture(GL_TEXTURE5);
			glBindTexture(GL_TEXTURE_2D, miscTextureB);
		}
		else {
			glActiveTexture(GL_TEXTURE4);
			glBindTexture(GL_TEXTURE_2D, miscTextureB);
			glActiveTexture(GL_TEXTURE5);
			glBindTexture(GL_TEXTURE_2D, miscTextureA);
		}
		glActiveTexture(GL_TEXTURE6);
		glBindTexture(GL_TEXTURE_2D, normalTexture);
		glActiveTexture(GL_TEXTURE7);
		glBindTexture(GL_TEXTURE_2D, depthTexture);
		glActiveTexture(GL_TEXTURE8);
		glBindTexture(GL_TEXTURE_2D, randomTexture);

		glUniform1i(postProcessShader.uniforms[U_OUT_TX], 0);
		glUniform1i(postProcessShader.uniforms[U_EDGE_TX], 1);
		glUniform1i(postProcessShader.uniforms[U_LARGE_HORIZONTAL_BLUR_TX],2);
		glUniform1i(postProcessShader.uniforms[U_SMALL_HORIZONTAL_BLUR_TX],3);
		glUniform1i(postProcessShader.uniforms[U_MISC_TX],4);
		glUniform1i(postProcessShader.uniforms[U_LAST_MISC_TX],5);
		glUniform1i(postProcessShader.uniforms[U_NORMAL_TX],6);
		glUniform1i(postProcessShader.uniforms[U_DEPTH_TX],7);
		glUniform1i(postProcessShader.uniforms[U_RANDOM_TX],8);

		mat4 invPersp = inverse(persp);
		glUniformMatrix4fv(postProcessShader.uniforms[U_INV_PERSP], 1, GL_FALSE, &invPersp[0][0]);
		
		glUniform1i(postProcessShader.uniforms[U_SCREEN_WIDTH], width);
		glUniform1i(postProcessShader.uniforms[U_SCREEN_HEIGHT], height);
		glUniform1f(postProcessShader.uniforms[U_FAR], farPlane);
		glUniform1f(postProcessShader.uniforms[U_NEAR], nearPlane);

		glDrawElements(GL_TRIANGLES, screen_quad_VBO.num_indices, GL_UNSIGNED_SHORT, 0);
	}

	glBindVertexArray(0);
}